#    Rubatom
#    Copyright (c) 2014 Sebastian Dufner
#
#    This file is part of Rubatom.
#
#    Rubatom is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    Rubatom is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.

module Rubatom
  class Score
    class Wrapper < self
      def initialize(score_functions, method)
        @score_functions, @method = score_functions, method
      end

      ##
      # Method for consistency. Calling Score#method(a, b) is identical to
      # calling Score#method[a, b].

      def [](expectation, deviation)
        send @method, expectation, deviation
      end

      ##
      # Returns a proc that returns 1 if the desired attribute is smaller than
      # +x+. Otherwise, the proc will return the value of the normal
      # distribution at position +x+.
      #
      # :call-seq: < [expectation, deviation]

      def < (args)
        expectation, deviation = *args

        f = Score.nnd(expectation, deviation)
        ff = proc {|x| x < expectation ? 1 : f[x]}
        @score_functions[@method] = [ff]
      end

      ##
      # Returns a proc that returns 1 if the desired attribute is greater than
      # +x+. Otherwise, the proc will return the value of the normal
      # distribution at position +x+.
      #
      # :call-seq: > [expectation, deviation]

      def > (args)
        expectation, deviation = *args

        f = Score.nnd(expectation, deviation)
        ff = proc {|x| x > expectation ? 1 : f[x]}
        @score_functions[@method] = [ff]
      end
    end
    private_constant :Wrapper

    ##
    # Returns a proc that computes the value of the Normal Distribution for
    # its parameter +x+.

    def self.gaussian_normal_distribution(expectation, deviation)
      a = deviation * Math::sqrt(2 * Math::PI)
      b = 2.0 * deviation ** 2

      proc {|x| Math::exp(-((x - expectation) ** 2) / b) / a }
    end
    def self.nd(x, d); gaussian_normal_distribution(x, d); end

    ##
    # Returns a proc that computes the value of the Normal Distribution for its
    # parameter +x+, normalized to 1 (f(expectation) = 1).

    def self.normalized_normal_distribution(expectation, deviation)
      f = gaussian_normal_distribution(expectation, deviation)
      p = 1.0 / f[expectation]

      proc {|x| p * f[x]}
    end
    def self.nnd(x, d) normalized_normal_distribution(x, d); end

    def initialize(&block)
      @score_functions = {}
      instance_exec &block
    end

    def affiliation(affiliation)
      @score_functions[:affiliation_score] = [proc {|x| x}, affiliation]
    end

    def method_missing(method, *args)
      return Wrapper.new(@score_functions, method) if args.empty?

      f = self.class.nnd(*args)
      @score_functions[method] = [f]
    end

    def to_proc
      funcs = @score_functions

      proc do
        funcs.map {|m, args| args[0][send(m, *args[1, args.size])]}
            .inject(1) {|acc, score| acc * score}
      end
    end
  end
end
